﻿using ILRuntime.Mono.Cecil.Cil;
using System;

namespace ILRuntime.Mono.Cecil
{
    internal sealed class GenericParameterResolver
    {
        internal static TypeReference ResolveReturnTypeIfNeeded(MethodReference methodReference)
        {
            if (methodReference.DeclaringType.IsArray && methodReference.Name == "Get")
                return methodReference.ReturnType;

            GenericInstanceMethod genericInstanceMethod = methodReference as GenericInstanceMethod;
            GenericInstanceType declaringGenericInstanceType = methodReference.DeclaringType as GenericInstanceType;

            if (genericInstanceMethod == null && declaringGenericInstanceType == null)
                return methodReference.ReturnType;

            return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, methodReference.ReturnType);
        }

        internal static TypeReference ResolveFieldTypeIfNeeded(FieldReference fieldReference)
        {
            return ResolveIfNeeded(null, fieldReference.DeclaringType as GenericInstanceType, fieldReference.FieldType);
        }

        internal static TypeReference ResolveParameterTypeIfNeeded(MethodReference method, ParameterReference parameter)
        {
            GenericInstanceMethod genericInstanceMethod = method as GenericInstanceMethod;
            GenericInstanceType declaringGenericInstanceType = method.DeclaringType as GenericInstanceType;

            if (genericInstanceMethod == null && declaringGenericInstanceType == null)
                return parameter.ParameterType;

            return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, parameter.ParameterType);
        }

        internal static TypeReference ResolveVariableTypeIfNeeded(MethodReference method, VariableReference variable)
        {
            GenericInstanceMethod genericInstanceMethod = method as GenericInstanceMethod;
            GenericInstanceType declaringGenericInstanceType = method.DeclaringType as GenericInstanceType;

            if (genericInstanceMethod == null && declaringGenericInstanceType == null)
                return variable.VariableType;

            return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, variable.VariableType);
        }

        private static TypeReference ResolveIfNeeded(IGenericInstance genericInstanceMethod, IGenericInstance declaringGenericInstanceType, TypeReference parameterType)
        {
            ByReferenceType byRefType = parameterType as ByReferenceType;
            if (byRefType != null)
                return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, byRefType);

            ArrayType arrayType = parameterType as ArrayType;
            if (arrayType != null)
                return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, arrayType);

            GenericInstanceType genericInstanceType = parameterType as GenericInstanceType;
            if (genericInstanceType != null)
                return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, genericInstanceType);

            GenericParameter genericParameter = parameterType as GenericParameter;
            if (genericParameter != null)
                return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, genericParameter);

            RequiredModifierType requiredModifierType = parameterType as RequiredModifierType;
            if (requiredModifierType != null && ContainsGenericParameters(requiredModifierType))
                return ResolveIfNeeded(genericInstanceMethod, declaringGenericInstanceType, requiredModifierType.ElementType);

            if (ContainsGenericParameters(parameterType))
                throw new Exception("Unexpected generic parameter.");

            return parameterType;
        }

        private static TypeReference ResolveIfNeeded(IGenericInstance genericInstanceMethod, IGenericInstance genericInstanceType, GenericParameter genericParameterElement)
        {
            return (genericParameterElement.MetadataType == MetadataType.MVar)
                ? (genericInstanceMethod != null ? genericInstanceMethod.GenericArguments[genericParameterElement.Position] : genericParameterElement)
                : genericInstanceType.GenericArguments[genericParameterElement.Position];
        }

        private static ArrayType ResolveIfNeeded(IGenericInstance genericInstanceMethod, IGenericInstance genericInstanceType, ArrayType arrayType)
        {
            return new ArrayType(ResolveIfNeeded(genericInstanceMethod, genericInstanceType, arrayType.ElementType), arrayType.Rank);
        }

        private static ByReferenceType ResolveIfNeeded(IGenericInstance genericInstanceMethod, IGenericInstance genericInstanceType, ByReferenceType byReferenceType)
        {
            return new ByReferenceType(ResolveIfNeeded(genericInstanceMethod, genericInstanceType, byReferenceType.ElementType));
        }

        private static GenericInstanceType ResolveIfNeeded(IGenericInstance genericInstanceMethod, IGenericInstance genericInstanceType, GenericInstanceType genericInstanceType1)
        {
            if (!ContainsGenericParameters(genericInstanceType1))
                return genericInstanceType1;

            GenericInstanceType newGenericInstance = new GenericInstanceType(genericInstanceType1.ElementType);

            foreach (TypeReference genericArgument in genericInstanceType1.GenericArguments)
            {
                if (!genericArgument.IsGenericParameter)
                {
                    newGenericInstance.GenericArguments.Add(ResolveIfNeeded(genericInstanceMethod, genericInstanceType, genericArgument));
                    continue;
                }

                GenericParameter genParam = (GenericParameter)genericArgument;

                switch (genParam.Type)
                {
                    case GenericParameterType.Type:
                        {
                            if (genericInstanceType == null)
                                throw new NotSupportedException();

                            newGenericInstance.GenericArguments.Add(genericInstanceType.GenericArguments[genParam.Position]);
                        }
                        break;

                    case GenericParameterType.Method:
                        {
                            if (genericInstanceMethod == null)
                                newGenericInstance.GenericArguments.Add(genParam);
                            else
                                newGenericInstance.GenericArguments.Add(genericInstanceMethod.GenericArguments[genParam.Position]);
                        }
                        break;
                }
            }

            return newGenericInstance;
        }

        private static bool ContainsGenericParameters(TypeReference typeReference)
        {
            GenericParameter genericParameter = typeReference as GenericParameter;
            if (genericParameter != null)
                return true;

            ArrayType arrayType = typeReference as ArrayType;
            if (arrayType != null)
                return ContainsGenericParameters(arrayType.ElementType);

            PointerType pointerType = typeReference as PointerType;
            if (pointerType != null)
                return ContainsGenericParameters(pointerType.ElementType);

            ByReferenceType byRefType = typeReference as ByReferenceType;
            if (byRefType != null)
                return ContainsGenericParameters(byRefType.ElementType);

            SentinelType sentinelType = typeReference as SentinelType;
            if (sentinelType != null)
                return ContainsGenericParameters(sentinelType.ElementType);

            PinnedType pinnedType = typeReference as PinnedType;
            if (pinnedType != null)
                return ContainsGenericParameters(pinnedType.ElementType);

            RequiredModifierType requiredModifierType = typeReference as RequiredModifierType;
            if (requiredModifierType != null)
                return ContainsGenericParameters(requiredModifierType.ElementType);

            GenericInstanceType genericInstance = typeReference as GenericInstanceType;
            if (genericInstance != null)
            {
                foreach (TypeReference genericArgument in genericInstance.GenericArguments)
                {
                    if (ContainsGenericParameters(genericArgument))
                        return true;
                }

                return false;
            }

            if (typeReference is TypeSpecification)
                throw new NotSupportedException();

            return false;
        }
    }
}
